使用 Rxjs 替代 Redux (三)
前言
在前面两篇文章中,我们已经实现了用 Rxjs 替代 Redux 的整个流程,上手开发业务也没有问题了。然而,这套方法有没有什么可以优化的地方呢?
重复的代码,DRY
随着项目的增大,你会慢慢发现这些代码的出现
// userList 的 loading 状态的流
export const userListLoading$ = new BehaviorSubject(false).debug('[user]:[userListLoading]');
// userList 的数据的流
export const userList$ = new BehaviorSubject([]).debug('[user]:[userList]');
// 加载 userList 的 action
export const loadUserList = params => {
// loading
userListLoading$.next(true);
request({
method: 'GET',
url: '/user',
data: params,
sucCallback: (data) => {
// 获取到数据,让 store 执行 next
userList$.next(data);
},
errCallback: (err) => {
// 错误处理
showErrorToast(err);
}
})
};
接下来,如果有其他数据的增加,对应的代码很类似,只是流和 action 的命名不同而已
DRY 是 Don’t Repeat Yourself 的缩写。意思是说,在一个设计里,对于任何东西,都应该有且只有一个表示,其它的地方都应该引用这一处。这样需要改动的时候,只需调整这一处,所有的地方就都变更过来了。
可见,我们的设计其实有点违背 DRY 原则,重复的代码过多了!
实现 entity_generator
参照之前编写 redux generator 的经验,我们来实现一个 entity_generator,使用配置的方式生成我们需要的流和 action,从而精简代码,提高开发效率
普通数据类型
export const data$ = new BehaviorSubject(0).debug('[example]:[data]');
- 变化的有:流的名字 data$、初始值、以及打印的前缀
- example 属于 entity,其下包括多个数据流以及 action,data 属于其中一个数据流
- 我们想实现的是对于每一个 entity,通过配置,生成其下的所有数据流和 action
- 每一个配置是一个 json
具体的实现
enum type {
DATA = 'data',
VALUE_DATA = 'valueData',
BOOL_DATA = 'boolData',
ACTION = 'action',
ASYNC_ACTION = 'asyncAction'
}
interface IConfig {
type: type;
name: string;
init?: any;
privateName?: boolean;
handler?: (params?, entity?) => any;
privateLoading?: boolean;
actionLoading?: boolean;
confirmLoading?: boolean;
bindLoading?: boolean;
requestOpts?: any;
filter?: {
init: any;
};
data?: {
init: any;
handler: (params?, response?, entity?) => any;
}
}
type IConfigs = IConfig[];
// 暴露一个方法,传入 entityName 和配置数组,生成对应的 entity
export default (entityName: string, configs: IConfigs) => {
// 用一个 result 来存储 entity
const result: any = {};
// 遍历配置,生成数据
configs.forEach(config => {
// 获取配置中的属性
const {
type, // 类型
name, // 名称
init // 初始值
} = config;
// 根据 type 来生成数据
switch (config.type) {
case actionTypes.DATA:
/** create data Observable */
result[`${name}$`] = new BehaviorSubject(init || false).debug(`[${entityName}]:[${name}]`);
break;
}
});
return result;
};
对应的配置
{
type: actionTypes.DATA,
name: 'data',
init: 0
}
值类型
export const data$ = new BehaviorSubject(0).debug('[example]:[data]');
export const setData = params => {
data$.next(params);
};
- 变化的有:名称、初始值、以及生成的 action 名字
- 我们约定设置该值的 action 名字规则为 setXXX
- 我们需要一个转换驼峰式写法的函数如下
const toCamel = src => `${src[0].toUpperCase()}${src.substr(1, src.length)}`;
具体的实现
case actionTypes.VALUE_DATA:
/** create data Observable */
result[`${name}$`] = new BehaviorSubject(init || 0).debug(`[${entityName}]:[${name}]`);
/** set data operation */
result[`set${toCamel(name)}`] = params => {
result[`${name}$`].next(params);
};
break;
对应的配置
{
type: actionTypes.VALUE_DATA,
name: 'data',
init: 0
}
布尔类型
export const modalShow$ = new BehaviorSubject(false).debug('[example]:[modalShow]');
export const showModal = () => {
modalShow$.next(true);
};
export const hideModal = () => {
modalShow$.next(false);
};
- 变化的有:名称、初始值、以及生成的 action 名字
- 我们约定设置该值的数据流名字为 xxxShow$,action 名字规则为 setXXX
具体的实现
case actionTypes.BOOL_DATA:
/** create data Observable. */
result[`${name}Show$`] = new BehaviorSubject(init || false).debug(`[${entityName}]:[${name}Show]`);
/** show operation */
result[`show${toCamel(name)}`] = () => {
result[`${name}Show$`].next(true);
};
/** hide operation */
result[`hide${toCamel(name)}`] = () => {
result[`${name}Show$`].next(false);
};
break;
对应的配置
{
type: actionTypes.BOOL_DATA,
name: 'modal',
init: false
}
action 类型
export const doSomething = params => data$.next('Change');
- 变化的有:action 名字以及对应的操作
- 我们用一个 handler(params, result) 函数来实现操作
具体的实现
case actionTypes.ACTION:
/** create operation */
result[`${name}`] = params => {
handler(params, result);
};
break;
对应的配置
{
type: actionTypes.ACTION
name: 'doSomething',
handler: (params, entity) => entity.data$.next('Change');
}
异步 action 类型
// loading、data
export const listLoading$ = new BehaviorSubject(false).debug('[example]:[listLoading]');
export const listData$ = new BehaviorSubject([]).debug('[example]:[listData]');
// 过滤器
export const listFilterData$ = new BehaviorSubject({}).debug('[example]:[listFilterData]');
export const setListFilterData = data => listFilterData$.next(data);
// 分页
export const listCurPage$ = new BehaviorSubject(1).debug('[example]:[listCurPage]');
export const setListCurPage = data => listCurPage$.next(data);
export const listPageSize$ = new BehaviorSubject(20).debug('[example]:[listPageSize]');
export const setListPageSize = data => listPageSize$.next(data);
export const listTotal$ = new BehaviorSubject(0).debug('[example]:[listTotal]');
// 请求
export const list = params => {
listLoading$.next(true);
request({
method: 'GET',
url: '/list',
data: params,
sucCallback: (data) => {
listLoading$.next(false);
listData$.next(data.list);
listTotal$.next(data.count);
},
errCallback: (err) => {
listLoading$.next(false);
showErrorToast(err);
}
})
};
只要做一下约定,就可以让以上数据标准化。
具体的实现
case actionTypes.ASYNC_ACTION:
/** private loading,auto emit data in async operation */
if (privateLoading) {
result[`${name}Loading$`] = new BehaviorSubject(false).debug(`[${entityName}]:[${name}Loading]`);
}
/** create data Observable,auto emit data when request success */
if (data) {
result[`${name}Data$`] = new BehaviorSubject(data.init).debug(`[${entityName}]:[${name}Data]`);
}
/** create filter Observable */
if (filter) {
result[`${name}FilterData$`] = new BehaviorSubject(filter.init).debug(`[${entityName}]:[${name}FilterData]`);
result[`set${toCamel(name)}FilterData`] = data => {
result[`${name}FilterData$`].next(data);
};
}
if (pagination) {
result[`${name}CurPage$`] = new BehaviorSubject(1).debug(`[${entityName}]:[${name}CurPage]`);
result[`set${toCamel(name)}CurPage`] = data => {
result[`${name}CurPage$`].next(data);
};
result[`${name}PageSize$`] =
new BehaviorSubject(pagination.pageSize || 20).debug(`[${entityName}]:[${name}PageSize]`);
result[`set${toCamel(name)}PageSize`] = data => {
result[`${name}PageSize$`].next(data);
};
result[`${name}Total$`] = new BehaviorSubject(0).debug(`[${entityName}]:[${name}Total]`);
}
/** create the main operation */
result[name] = (params?) => {
/** loading => true */
privateLoading && result[`${name}Loading$`].next(true);
request({
method: requestOpts.method,
url: requestOpts.url(params, result),
/** requestOpts.data is a function that return data according to params and the entity */
data: requestOpts.data && requestOpts.data(omit(['sucCallback', 'errCallback'], params), result),
sucCallback: response => {
/** loading => false */
privateLoading && result[`${name}Loading$`].next(false);
/** set data according to response */
data && result[`${name}Data$`].next(data.handler(params, response, result));
/** set total according to response if pagination */
pagination && result[`${name}Total$`].next(pagination.total(params, response, result));
/** do callback if needed */
requestOpts.sucCallback && requestOpts.sucCallback(params, response, result);
},
errCallback: err => {
/** loading => false */
privateLoading && result[`${name}Loading$`].next(false);
/** do callback if needed */
requestOpts.errCallback && requestOpts.errCallback(params, err, result);
}
});
};
break;
对应的配置
{
type: actionTypes.ASYNC_ACTION,
name: 'list',
privateLoading: true,
requestOpts: {
method: 'GET',
url: (params, entity) => `/list`,
data: (params, entity) => Object.assign({},
entity[`listFilterData$`].getValue(), {
limit: entity[`listPageSize$`].getValue(),
offset: (entity[`listCurPage$`].getValue() - 1) * entity[`listPageSize$`].getValue()
}, params)
},
filter: {
init: {}
},
pagination: {
total: (params, response, entity) => response.count
},
data: {
init: [],
handler: (params, response, entity) => response.list
}
}
总结
通过实现以上的 entity_generator,在开发过程中不仅节省了代码,也提高了开发效率,约定了标准的命名规范,最终提升了协同的效率。
记住,Don’t repeat yourselef.